技巧54 传统方式:搭配make和Docker
有时候,用户可能会发现有些Dockerfile限制了自己的构建流程。举个例子,如果限制自己执行 dockerbuild
命令,就无法产出任何输出 文件 ,并且无法在Dockerfile里定义变量。
这种附加的工具化需求可以通过一些工具(包括纯shell脚本)来实现。在本技巧里,我们将一起来看看可以怎样结合老牌的make工具与Docker一起工作。
问题
想要在 dockerbuild
执行过程中增加额外的任务。
解决方案
使用一个古老的(计算机术语中)工具make。
为了避免用户之前没有make使用经验,我们在这里对它先做一些简单的介绍,make是一款工具,它需要一个或多个输入文件并且会产出一个输出文件,但是它也可以用作一个任务执行器。代码清单7-4给出的是一个简单的示例(注意所有缩进都必须是制表符)。
代码清单7-4 一个简单的Makefile
.PHONY: default createfile catfile ⇽--- 默认情况下,make会假定所有目标均是将被任务创建的文件名,使用.PHONY表明这不是任务的真正名称
default: createfile ⇽--- 按照惯例,Makefile中的第一个目标是default。如果在运行的时候没有指定一个明确的目标,make将会选取文件中的第一个目标。可以看到,因为createfile是default的唯一依赖,default将会执行它
createfile: x.y.z ⇽--- createfile是一个伪任务,它依赖x.y.z任务
catfile: ⇽--- catfile是一个伪任务,它执行单条命令
cat x.y.z
x.y.z: ⇽--- x.y.z是一个文件任务,会执行两条命令并创建目标x.y.z文件
echo "About to create the file x.y.z"
echo abc > x.y.z
警告
一个Makefile里的所有缩进都必须是制表符,并且目标里的每条命令都是在不同的shell里执行的(所以环境变量不会被传递过去)。
一旦在名为Makefile的文件中定义了上述内容,便可以使用像 make createfile
这样的命令去调用任意目标。
现在我们可以在Makefile中查看一些有用的模式——接下来要讨论的目标都将是伪任务,因为它很难(尽管可以)通过追踪文件的变动来自动触发Docker构建。Dockerfile会对镜像层进行缓存,因此构建往往会很快。
第一步就是运行一个Dockerfile。因为Makefile是由shell命令组成的,所以这一点很容易办到,如代码清单7-5所示。
代码清单7-5 创建一个镜像的Dockerfile
base:
docker build -t corp/base .
上述命令做的工作带来的一些正常变动正是用户所期许的结果(例如,将文件通过管道传递给 docker build
以删除上下文,或是用 -f
指定采用不同命名的Dockerfile),而且用户可以使用 make
的依赖功能,在必要时自动构建基础镜像(在 FROM
中使用的那个)。例如,如果用户在一个叫repos的子目录下迁出几个仓库(这样也容易做 make
),用户可以像代码清单7-6所示这样添加一个目标。
代码清单7-6 在子目录里构建镜像的Makefile
app1: base
cd repos/app1 && docker build -t corp/app1 .
这样做的缺点是,每当基础镜像需要重新构建时,Docker就需要上传一个包含所有依赖仓库的构建上下文。可以通过显式地传入一个作为构建上下文的TAR文件给Docker来解决这一问题,如代码清单7-7所示。
代码清单7-7 用特定文件集合构建镜像的Dockerfile
base:
tar -cvf - file1 file2 Dockerfile | docker build -t corp/base -
如果用户目录内包含大量与构建无关的文件,那么这种依赖的显式声明语句将会带来一个显著的速度方面的提升。如果用户想要将所有构建依赖保留在不同的目录里,可以稍微修改一下这个目标,如代码清单7-8所示。
代码清单7-8 用重命名路径下特定文件集合构建镜像的Makefile
base:
tar --transform 's/^deps\///' -cf - deps/* Dockerfile | \
docker build -t corp/base -
在这里,用户可以将deps目录下的所有内容添加到构建上下文中,然后使用 --transform
选项压缩 tar
包(Linux上的最新 tar
版本支持),这样便可以从文件名中除去任何前导“deps/”。在这个例子里,更好的办法是将deps和Dockerfile放在各自的目录中以允许正常的 docker build
,但是了解这种高级用法很有价值,因为它可以在一些最不可能的地方派上用场。在使用这一方案之前往往要考虑清楚,毕竟它会增加构建流程的复杂度。
简单的变量替换是一件相对简单的事情,但是(如之前的 --transform
)在使用它之前还是得考虑清楚——Dockerfile之所以故意不支持变量,就是为了保持构建是易于重现的。这里我们将用到传给 make
的一些变量,然后使用 sed
替换,不过用户也可以按照自己的喜好来传参和替换,如代码清单7-9所示。
代码清单7-9 使用基本Dockerfile变量替换构建镜像的Makefile
VAR1 ?= defaultvalue
base:
cp Dockerfile.in Dockerfile
sed -i 's/{VAR1}/$(VAR1)/' Dockerfile
docker build -t corp/base .
Dockerfile将在每次基础目标运行时被重新生成,而且用户可以通过添加更多的 sed -i
条目添加更多的变量替换。要覆盖 VAR1
的默认值,可以执行 make VAR1 = newvalue base
。倘若变量里面包含斜杠,用户可能需要另外指定一个 sed
分隔符,如 sed -i's#VAR1}#$(VAR1)#'Dockerfile
。
最后,如果用户一直使用Docker作为构建工具,那便需要知道怎样才能从Docker中获取文件。我们将介绍几种不同的可选方案,具体取决于实际用例场景,如代码清单7-10所示。
代码清单7-10 从镜像中复制出文件的Makefile
singlefile: base
docker run --rm corp/base cat /path/to/myfile > outfile
multifile: base
docker run --rm -v $(pwd)/outdir:/out corp/base sh \
-c "cp -r /path/to/dir/* /out/"
在这里, singlefile
对一个文件执行 cat
,然后管道输出到一个新文件。这个方案具有自动设置正确的文件拥有者的优点,但是对于多个文件的处理就会变得很麻烦。推荐的 multifile
方案则是在容器里挂载一个卷并将所有文件从一个目录复制到该卷。用户可以使用 chown
命令来设置文件的真正拥有者,但是别忘了在调用时可能需要带上 sudo
。
Docker项目本身从源代码构建Docker时便是用的挂载卷的方案。
讨论
在这样一本讨论Docker这种较新技术的书中出现像make这样古老的工具似乎是一件比较奇怪的事情。为什么不使用新一点的技术(如Ant、Maven或其他可用的通用构建工具)呢?
答案是,尽管make有一些缺点,但是它有以下优势:
- 在短期内不会被弃用;
- 有良好的文档建设;
- 有很强的灵活性;
- 使用非常广泛。
我们花费了许多时间在解决较新的构建技术的bug以及一些文档很少(或者压根就没有文档)的功能限制上,尝试安装这些新系统的依赖也很费时。而make的特性拯救了我们很多次。再者,5年以后make很大概率仍然可以继续使用,而其他工具更有可能已经消失,或者它们的负责人不再维护它们了。